﻿using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.Debugger.Interop;
using Red.CommUtils;
using Red.Scripts.Debugger.Defines;

namespace Red.Scripts.Debugger.Remote.Debugger.Breakpoint
{
    public class Bound : IDebugBoundBreakpoint2
    {
        #region Private Data

        private Location _request;
        private Location _confirmed;
        private BP_PASSCOUNT _passCount;
        private BP_CONDITION _condition;

        #endregion Private Data

        #region Public Properties

        public IDebugPendingBreakpoint2 Parent { get; private set; }
        public Program Program { get; private set; }
        public IChannel Channel { get; private set; }
        public IPostman Postman { get; private set; }
        public enum_BP_STATE State { get; private set; }
        public BP_REQUEST_INFO2 RequestInfo { get; private set; }
        public uint HitCount { get; set; } = 0;

        public bool IsEnabled
        {
            get { return State == enum_BP_STATE.BPS_ENABLED; }
            set { Enable( value ); }
        }

        #endregion Public Properties

        #region C-Tor

        public Bound( IDebugPendingBreakpoint2 parent, Program program, IChannel channel, IPostman postman )
        {
            Parent = parent;
            Program = program;
            Channel = channel;
            Postman = postman;

            Channel.RegisterListener( OnMessage );

            AnalyseBreakpointRequest();
        }

        #endregion C-Tor

        #region Public Methods

        public void StartBind()
        {
            Debug.Assert( State == enum_BP_STATE.BPS_NONE );

            SendEngineRequest( true );
        }

        internal bool CheckConfirmed( Location location )
        {
            return _confirmed.CompareRelative( location );
        }

        #endregion Public Methods

        #region Private Methods

        private void SetBindSuccessful( bool success )
        {
            if ( State == enum_BP_STATE.BPS_NONE )
            {
                if ( success )
                {
                    State = enum_BP_STATE.BPS_ENABLED;

                    Postman.SendBreakpointBoundEvent( Parent, this );

                    BindSuccessful?.Invoke( this );

                    SendCondition();
                    SendPassCount();
                }
                else
                {
                    Resolution.StandardCodeContext codeContext = CreateCodeContext();

                    Resolution.StandardError resolution = new Resolution.StandardError( "No code on this line", codeContext );
                    Defines.Breakpoint.Error error = new Defines.Breakpoint.Error( Parent, resolution );

                    Postman.SendBreakpointErrorEvent( Program, error );

                    Error?.Invoke( this, error );
                }
            }
        }

        private Resolution.StandardCodeContext CreateCodeContext()
        {
            Resolution.StandardDocumentContext documentContext = new Resolution.StandardDocumentContext( _confirmed );
            return new Resolution.StandardCodeContext( documentContext, Program );
        }

        private void Enable( bool enable )
        {
            Debug.Assert( State != enum_BP_STATE.BPS_NONE );

            if ( State != enum_BP_STATE.BPS_DELETED )
            {
                enum_BP_STATE newState = enable ? enum_BP_STATE.BPS_ENABLED : enum_BP_STATE.BPS_DISABLED;

                if ( State != newState )
                {
                    State = newState;

                    SendEngineRequest( IsEnabled );

                    SendCondition();
                    SendPassCount();
                }
            }
        }

        private void AnalyseBreakpointRequest()
        {
            IDebugBreakpointRequest2 request;

            if ( Parent.GetBreakpointRequest( out request ) == Constants.S_OK )
            {
                IDebugBreakpointRequest3 requestEx = (IDebugBreakpointRequest3)request;

                BP_REQUEST_INFO2[] requestInfo = new BP_REQUEST_INFO2[ 1 ];
                if ( requestEx.GetRequestInfo2( enum_BPREQI_FIELDS.BPREQI_ALLFIELDS, requestInfo ) == Constants.S_OK )
                {
                    RequestInfo = requestInfo[ 0 ];

                    Debug.Assert( RequestInfo.bpLocation.bpLocationType == (uint)enum_BP_LOCATION_TYPE.BPLT_CODE_FILE_LINE );

                    IDebugDocumentPosition2 docPosition = (IDebugDocumentPosition2)( Marshal.GetObjectForIUnknown( RequestInfo.bpLocation.unionmember2 ) );

                    _request = Location.CreateFromVisualStudio( docPosition );

                    AnalyseRequestForPasscount();
                    AnalyseRequestForCondition();
                }
            }
        }

        private void AnalyseRequestForPasscount()
        {
            if ( RequestInfo.dwFields.HasFlag( enum_BPREQI_FIELDS.BPREQI_PASSCOUNT ) )
            {
                _passCount = RequestInfo.bpPassCount;
                SendPassCount();
            }
        }

        private void AnalyseRequestForCondition()
        {
            if ( RequestInfo.dwFields.HasFlag( enum_BPREQI_FIELDS.BPREQI_CONDITION ) )
            {
                _condition = RequestInfo.bpCondition;
                SendCondition();
            }
        }

        private void SendPassCount()
        {
            if ( !IsEnabled || _passCount.stylePassCount == enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE )
                return;

            comm.ScriptBreakpointPasscount request = new comm.ScriptBreakpointPasscount();

            request.passcount = _passCount.dwPassCount;
            request.style = (byte)_passCount.stylePassCount;
            _request.CopyTo( request.breakpoint );

            Channel.Send( request );
        }

        private void SendCondition()
        {
            if ( !IsEnabled || _condition.styleCondition == enum_BP_COND_STYLE.BP_COND_NONE )
                return;

            comm.ScriptBreakpointCondition conditionMessage = new comm.ScriptBreakpointCondition();

            conditionMessage.expression = _condition.bstrCondition;
            conditionMessage.style = (byte)_condition.styleCondition;
            _request.CopyTo( conditionMessage.breakpoint );

            Channel.Send( conditionMessage );
        }

        private string ConvertFileToScriptPath( string file )
        {
            // Hack - Need to figure out the correct way to convert an absolute path to a relative script-path
            int index = file.IndexOf( "scripts" );
            return file.Substring( index + "scripts/".Length );
        }

        private void SendEngineRequest( bool enabled )
        {
            comm.ScriptBreakpointRequest request = new comm.ScriptBreakpointRequest();

            request.enabled = enabled;
            _request.CopyTo( request.breakpoint );

            Channel.Send( request );
        }

        #endregion Private Methods

        #region Event Handlers

        private void OnMessage( comm.Message message, IResponse responseGateway )
        {
            if ( message.IsA( comm.EMessageID.ScriptBreakpointConfirmation ) )
            {
                comm.ScriptBreakpointConfirmation confirmation = (comm.ScriptBreakpointConfirmation)message;

                Location request = Location.CreateFromRedEngine( confirmation.request );

                if( _request.CompareAbsolute( request ) )
                {
                    _confirmed = Location.CreateFromRedEngine( confirmation.confirmed );

                    SetBindSuccessful( confirmation.success );
                }
            }
            else if ( message.IsA( comm.EMessageID.ScriptBreakpointUnbound ) )
            {
                comm.ScriptBreakpointUnbound unbound = (comm.ScriptBreakpointUnbound)message;

                Location location = Location.CreateFromRedEngine( unbound.breakpoint );

                if( location.CompareRelative( _confirmed ) )
                {
                    Postman.SendBreakpointUnboundEvent( this );
                }
            }
        }

        #endregion Event Handlers

        #region IDebugBoundBreakpoint2

        public int Delete()
        {
            if ( State != enum_BP_STATE.BPS_DELETED )
            {
                if ( IsEnabled )
                {
                    SendEngineRequest( false );
                }

                Deleted?.Invoke( this );

                Channel.UnregisterListener( OnMessage );
                Channel = null;
                Parent = null;

                State = enum_BP_STATE.BPS_DELETED;
            }

            return Constants.S_OK;
        }

        public int Enable( int fEnable )
        {
            Enable( fEnable != 0 );

            return Constants.S_OK;
        }

        public int GetBreakpointResolution( out IDebugBreakpointResolution2 ppBPResolution )
        {
            Resolution.StandardCodeContext codeContext = CreateCodeContext();

            ppBPResolution = new Resolution.Standard( codeContext );

            return Constants.S_OK;
        }

        public int GetHitCount( out uint pdwHitCount )
        {
            pdwHitCount = HitCount;

            if ( State == enum_BP_STATE.BPS_DELETED )
                return Constants.E_BP_DELETED;

            return Constants.S_OK;
        }

        public int GetPendingBreakpoint( out IDebugPendingBreakpoint2 ppPendingBreakpoint )
        {
            ppPendingBreakpoint = Parent;

            return Constants.S_OK;
        }

        public int GetState( enum_BP_STATE[] pState )
        {
            pState[ 0 ] = State;

            return Constants.S_OK;
        }

        public int SetCondition( BP_CONDITION bpCondition )
        {
            if ( State == enum_BP_STATE.BPS_DELETED )
                return Constants.E_BP_DELETED;

            _condition = bpCondition;
            SendCondition();

            return Constants.S_OK;
        }

        public int SetHitCount( uint dwHitCount )
        {
            HitCount = dwHitCount;

            if ( State == enum_BP_STATE.BPS_DELETED )
                return Constants.E_BP_DELETED;

            if ( IsEnabled )
            {
                comm.ScriptBreakpointSetHitcount request = new comm.ScriptBreakpointSetHitcount();
                request.hitcount = HitCount;
                _request.CopyTo( request.breakpoint );
                Channel.Send( request );
            }

            return Constants.S_OK;
        }

        public int SetPassCount( BP_PASSCOUNT bpPassCount )
        {
            if ( State == enum_BP_STATE.BPS_DELETED )
                return Constants.E_BP_DELETED;

            _passCount = bpPassCount;
            SendPassCount();

            return Constants.S_OK;
        }

        #endregion IDebugBoundBreakpoint2

        #region Events

        public delegate void BoundEvent( Bound breakpoint );
        public delegate void BoundErrorEvent( Bound breakpoint, Defines.Breakpoint.Error error );

        public event BoundEvent BindSuccessful;
        public event BoundErrorEvent Error;
        public event BoundEvent Deleted;

        #endregion Events
    }
}
